package org.noote.libs.sensors;

import java.util.List;

import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;

// http://blog.androgames.net/85/android-accelerometer-tutorial/

public class AccelerometerManager {

    private Sensor _sensor;
    private SensorManager _sensorManager;
    private AccelerometerListener _listener;
 
    /** indicates whether or not Accelerometer Sensor is supported */
    private boolean _bSupported = false;
    /** indicates whether or not Accelerometer Sensor is running */
    private boolean _bRunning = false;
 
    /** Accuracy configuration */
    boolean _bCheckShake = true;
    private float _fShakeThreshold     = 1.5f;
    private long _iShakeInterval     = 1000;
    
    // active position derivation from acceleration
    boolean _bComputePosition = true;

    private long _lastUpdate = 0;
    private long _lastShake = 0;

    private double [] _fAcceleration = new double[]{0, 0, 0};
    private double [] _fPreviousAcceleration = new double[]{0, 0, 0};
    private double [] _fCurrentSpeed = new double[]{0, 0, 0};
    private double [] _fCurrentPosition = new double[]{0, 0, 0};

    /**
     * Returns true if the manager is listening to orientation changes
     */
    public boolean isListening() {
        return _bRunning;
    }
 
    /**
     * Unregisters listeners
     */
    public void stopListening() {
    	_bRunning = false;
        try {
            if (_sensorManager != null && _sensorEventListener != null) {
                _sensorManager.unregisterListener(_sensorEventListener);
            }
        } catch (Exception e) {}
    }
 
    /**
     * Returns true if at least one Accelerometer _sensor is available
     */
    public boolean isSupported(Context ctx) {
        if (ctx != null) {
        	_sensorManager = (SensorManager) ctx.getSystemService(Context.SENSOR_SERVICE);
            List<Sensor> _sensors = _sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER);
            _bSupported = (_sensors.size() > 0);
        } else {
        	_bSupported = false;
        }
        return _bSupported;
    }
 
    /**
     * Configure the _listener for shaking
     * @param threshold
     *             minimum acceleration variation for considering shaking
     * @param interval
     *             minimum interval between to shake events
     */
    public void configureShake(int threshold, int interval) {
    	_bCheckShake = true;
        _fShakeThreshold = threshold;
        _iShakeInterval = interval;
    }
 
    /**
     * Registers a _listener and start listening
     * @param accelerometerListener
     *             callback for accelerometer events
     */
    public void startListening(Context ctx, AccelerometerListener accelerometerListener)
    {
        _sensorManager = (SensorManager) ctx.getSystemService(Context.SENSOR_SERVICE);
        List<Sensor> _sensors = _sensorManager.getSensorList(Sensor.TYPE_ACCELEROMETER);
        if (_sensors.size() > 0) {
            _sensor = _sensors.get(0);
            _bRunning = _sensorManager.registerListener(_sensorEventListener, _sensor, SensorManager.SENSOR_DELAY_GAME);
            _listener = accelerometerListener;
        }
    }
 
    /**
     * Configures threshold and interval
     * And registers a _listener and start listening
     * @param accelerometerListener
     *             callback for accelerometer events
     * @param threshold
     *             minimum acceleration variation for considering shaking
     * @param interval
     *             minimum interval between to shake events
     */
    public void startListening(Context ctx, AccelerometerListener accelerometerListener, int threshold, int interval) {
        configureShake(threshold, interval);
        startListening(ctx, accelerometerListener);
    }
 
    /**
     * The _listener that listen to events from the accelerometer _listener
     */
    private SensorEventListener _sensorEventListener = 
        new SensorEventListener() {
 
        @Override
		public void onAccuracyChanged(Sensor sensor, int accuracy) {}
 
        @Override
		public void onSensorChanged(SensorEvent event) {
            
        	if (event.accuracy == SensorManager.SENSOR_STATUS_UNRELIABLE)
                return;
        	
        	// use the event timestamp as reference
            // so the manager precision won't depends 
            // on the AccelerometerListener implementation
            // processing time

        	//long now = event.timestamp/1e6; // nano to milli
            long now = System.currentTimeMillis();
 
    		_fAcceleration[0] = event.values[0];
    		_fAcceleration[1] = event.values[1];
    		_fAcceleration[2] = event.values[2];

            // wait to have a timed data before acting !
            if (_lastUpdate == 0)
            {
        		_fPreviousAcceleration[0] = _fAcceleration[0];
        		_fPreviousAcceleration[1] = _fAcceleration[1];
        		_fPreviousAcceleration[2] = _fAcceleration[2];
                _lastUpdate = now;
                _lastShake = now;
                return;
            }

            // get time difference
            long timeDiff = now - _lastUpdate;
            
            // no time between data update ?
            if (timeDiff <= 0) return;
            
            // have a shake movement ?
            if(_bCheckShake)
            {
                double force = (Math.abs(_fAcceleration[0] - _fPreviousAcceleration[0])+Math.abs(_fAcceleration[1] - _fPreviousAcceleration[1])+Math.abs(_fAcceleration[2] - _fPreviousAcceleration[2])) / timeDiff;
                if (force > _fShakeThreshold)
                {
                    if ((now - _lastShake) >= _iShakeInterval)
                    {
                        // trigger shake event
                        _listener.onShake((float)force);
                    }
                    _lastShake = now;
                }
            }
            
            // trigger change event
            _listener.onAccelerationChanged((float)_fAcceleration[0], (float)_fAcceleration[1], (float)_fAcceleration[2]);
            if(_bComputePosition)
            {
                // calcul position movement...
        		computeMovement(timeDiff/1e3); // milliseconde to seconde

            	_listener.onVelocityChanged((float)_fCurrentSpeed[0], (float)_fCurrentSpeed[1], (float)_fCurrentSpeed[2]);
            	_listener.onPositionChanged((float)_fCurrentPosition[0], (float)_fCurrentPosition[1], (float)_fCurrentPosition[2]);
            }

    		_fPreviousAcceleration[0] = _fAcceleration[0];
    		_fPreviousAcceleration[1] = _fAcceleration[1];
    		_fPreviousAcceleration[2] = _fAcceleration[2];
            _lastUpdate = now;
        }
        
    	// http://www.avrfreaks.net/index.php?name=PNphpBB2&file=viewtopic&t=106301
    	void computeMovement(double fTimeSlice)
    	{
    		// Differentiate
    		double xd = (_fAcceleration[0] - _fPreviousAcceleration[0]);
    		double yd = (_fAcceleration[1] - _fPreviousAcceleration[1]);
    		double zd = (_fAcceleration[2] - _fPreviousAcceleration[2]);

    		// round
    		xd = Math.round(xd * 50.0) / 50.0;
    		yd = Math.round(yd * 50.0) / 50.0;
    		zd = Math.round(zd * 50.0) / 50.0;

    		// enforce threshold
    		if ((Math.abs(xd) < 0.06) || (Math.abs(xd) > 6.0)) { xd = 0; _fCurrentSpeed[0] /= 2; }
    		if ((Math.abs(yd) < 0.06) || (Math.abs(yd) > 6.0)) { yd = 0; _fCurrentSpeed[1] /= 2; }
    		if ((Math.abs(zd) < 0.06) || (Math.abs(zd) > 6.0)) { zd = 0; _fCurrentSpeed[2] /= 2; }

    		// and reintegrated to get rid of gravity constant
    		double ax = xd * fTimeSlice;
    		double ay = yd * fTimeSlice;
    		double az = zd * fTimeSlice;

    		// integrate speed and distance
    		_fCurrentSpeed[0] += ax * fTimeSlice;
    		_fCurrentPosition[0] += _fCurrentSpeed[0] * fTimeSlice;

    		_fCurrentSpeed[1] += ay * fTimeSlice;
    		_fCurrentPosition[1] += _fCurrentSpeed[1] * fTimeSlice;

    		_fCurrentSpeed[2] += az * fTimeSlice;
    		_fCurrentPosition[2] += _fCurrentSpeed[2] * fTimeSlice;      
    	}
    };
 }